Skip to content

fix(pm): a population declaration's reason is whole, or the declaration is red - #18660

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-18422-dispatch-gates-whole-reason
Sep 17, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-18422-dispatch-gates-whole-reason

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18422

The defect

The three population markers in scripts/pm/dispatch-gates.mjs — NO_PATH_POPULATION_MARKER, WHOLE_TREE_POPULATION_MARKER and WIDE_POPULATION_MARKER — each captured their reason with (\S.*)$ under the m flag, so the capture ends at the first newline; a reason an author wraps across two or three comment lines was captured as line one only. Nothing refused it: read line by line, wholeTreePopulationRefusal checks that a reason EXISTS, that no sibling marker contradicts it, and that a root walk BACKS it — never that it is whole, and there was no other caller that did. The row rendered as a sentence that merely stops, with nothing red at author time or at read time, and the reason is the one thing a seat reads off that row when deciding whether a family belongs on its card. Measured on #17472 / PR #18414; measured again live in this tree, below.

All three markers shared the shape (WIDE_POPULATION_MARKER included — measured, not assumed), so the contract lands on all three.

Contract, before and after

before after
capture (\S.*)$ under m — ends at the first newline unchanged, and pinned as the defect
a wrapped reason reaches the seat as line one REFUSED, naming the declaration, the file and the line that continues it
what terminates a declaration nothing — the capture just stopped a blank line, a blank comment line, a non-comment line, EOF, or another dispatch-gates: declaration
consumers of the reason the fragment, silently the whole reason, or a refusal — alwaysRunsPopulationLines and the --json refused field for whole-tree and wide, the undetermined listing for no-path

The grammar now has ONE spelling (populationMarkerPattern), because the continuation reading has to agree with the capture about what a marker line is, down to the comment form; two spellings of one grammar drift silently. populationReasonContinuation reads, off the same text, the comment line that continues a reason; readPopulationDeclaration records the reason and its continuation from ONE source and ONE file, so a refusal can never grade file A's reason against file B's continuation; populationReasonCutRefusal is the refusal, shared by all three channels, and the whole-tree and wide refusals delegate to it.

Ordering, deliberate and pinned both ways: the cut is refused after the two-marker pair refusals (a declaration that contradicts a sibling is refused for THAT, in the words a reader has been getting for it) and before the whole-tree walk check and the wide hint check — both of those grade the declaration against a reason this reading says is only part of one, so a hint named in the wrapped half would read as unaccounted for and the refusal would name the wrong defect in confident words.

Shape B, and why — the four axes

Shape B (refuse at author time) over shape A (consume a comment block).

  • 实际业务需求 — measured over the tree at this head: 27 declarations across 25 gate files, 26 already writing the whole reason on one line (up to 1215 characters of it, check:route-envelope), and exactly one wrapped and being cut today. The one-line reason is what this convention already IS; the pull is for the one declaration to be made whole, not for a multi-line grammar nobody in this tree writes.
  • 项目长远合理性 — B is contract-first: a declaration has ONE spelling and the refusal states it. A adds a second grammar whose central question — is this following comment a continuation, or the next paragraph — nothing in the text can answer. This file refuses coin tosses everywhere else ("a derivation that picked either would be placing the family by a coin toss"); A is that coin toss, one channel further along.
  • 防 AI 写错元数据 — B is 契约收紧 with a loud refusal at author time naming the file and the line. A is 消费端宽容: it would silently make an unrelated comment part of a seat-facing reason — the same class of defect this card was filed on, pointed the other way, and unfalsifiable because there would be nothing to be red.
  • 创业阶段不扩散 — B is the smaller change and stays inside the refusals that already exist; A changes what a marker IS and grows the grammar for a spelling with no caller.

No axis conflicts, so there is no trade-off to hand up. The triage boundary (5713131801) is held: this is the capture domain and its validation, ⛔ not a marker that matches arbitrary multi-line text.

The pins

The file's --self-test registers cases with t(name, cond, detail) into one flat cases roster and holds the SELF_TEST_VERDICT handshake the dispatch refuses without; per docs/audits/2026-09-self-test-shape-census.md:377 its floor is NONE and its handshake is HELD — a recorded shape, unchanged here.

Registered: 25 new cases (fixture battery A declaration's reason is WHOLE, or the declaration is RED (#18422), plus three live-half pairs). Whole file after: ✓ dispatch-gates self-test: 1771 cases pass.

What they pin, clause by clause:

  • the [finding] git add -A over a worktree inside the checkout stages a mode-160000 gitlink at exit 0 with a warning — no repo-side gate refuses a gitlink that has no .gitmodules row #17472 first-draft shape as a case — the capture still ends at the first newline (pinned as the defect, not as a claim it went away), and the wholeness reading names the continuation line;
  • a one-line reason ⇒ unchanged — not continued by the code under it, nor by a blank line, a blank comment line, or EOF;
  • a following comment line that starts a NEW dispatch-gates: key ⇒ not a continuation (it is a second declaration; the pair refusals grade that shape);
  • the comment form must match — a # line under a // declaration is not a comment in that language;
  • the shell spelling read the same way, continuation and all;
  • a cut reason is REFUSED on each of the three channels, and the refusal names the declaration, the file and the line; a whole reason is refused nothing on any of them;
  • the two-marker pair refusals are unchanged by a cut reason — all three pairs still refuse as the contradiction they are;
  • the cut is named before the walk and before the hints;
  • the always-runs renderer prints a cut declaration as a REFUSED row rather than as the fragment it was cut down to;
  • the discovery keeps the FIRST declaration a family meets and the cut travels with it from the same file;
  • an unknown marker key is refused by both readings, and the field roster and the marker roster name the same three channels — neither can grow one alone.

The live derivation, over the tree at this head

Every declaration the three markers match today, run through the new contract:

LIVE DERIVATION: declarations=27 refused=1
RED check:objectui-bump [no-path-population] files=scripts/bump-objectui.selftest.sh
    declares no-path-population and its reason does not END on the marker line:
    scripts/bump-objectui.selftest.sh:51 continues it with "lives inside a disposable
    checkout under mktemp -d (a throwaway objectui and". …

One declaration reds — and it was being cut on main right now, not hypothetically: check:objectui-bump reached the seat as every path this file writes or reads, 36 characters of a 466-character reason wrapped over six comment lines. A refusal cannot land red on main, so the declaration is made whole on ONE line in this same PR:

  • scripts/bump-objectui.selftest.sh:50 — comment-only edit, the author's words joined verbatim at the wrap points (the join is asserted byte-for-byte against the original lines, not retyped). pnpm check:objectui-bump exit 0 after it.

After the repair: 27 declarations, 0 refused. The count is not vacuous — the refusal fired on this tree before the repair (above), and three live-half cases in the self-test re-fire it per channel by putting a continuation on a live entry and asserting the refusal names it.

Ablation, from the committed fix

Mutation: the refusal deleted — populationReasonCutRefusal returns null once it has a declaration, which is the pre-fix state exactly (the cut is detectable and nothing is red).

HEAD blob: 75fe87e3d6930a223fb97e034e3ca9c8496213dc
injected marker count: 1
mutated blob: f3f37ce1cce932dc73b4af5991d093a6063fcdcc
ABLATION RUN EXIT: 1
✗ dispatch-gates self-test: 8 of 1771 case(s) failed.   (633.3s under the shared lock)
restored blob: 75fe87e3d6930a223fb97e034e3ca9c8496213dc
RESTORE VERIFIED: blob == HEAD blob, git diff HEAD empty

Predicted direction before the run: 转红. Observed: 转红, 8 cases, and they are exactly the ones the refusal buys —

✗ a cut WHOLE-TREE reason is refused, and the refusal names the declaration, the file and the line that continues it
✗ a cut WIDE reason is refused through the same shared reading, naming its own channel
✗ and a cut NO-PATH reason is refused too — the channel with no refusal function of its own is not the channel without the contract
✗ but a cut reason IS named before the walk that would back it and before the hints it would be graded against
✗ the always-runs renderer prints a cut declaration as a REFUSED row rather than as the fragment it was cut down to
✗ and that reading is not vacuous over this tree: a live entry reds the moment a continuation is put on it            (no-path)
✗ and not vacuously: a live whole-tree entry reds the moment a continuation is put on it
✗ and not vacuously: a live wide-population entry reds the moment a continuation is put on it

The controls stayed GREEN under the same mutation, which is what makes these an instrument rather than a restatement: the two-marker pair refusals, a WHOLE reason is refused nothing on any of the three, every continuation-reading case (the detector still works — it is the refusal that was deleted, which is the card's whole point: the cut is knowable and nothing is red), and the three every live … reason ENDS on its own marker line cases (0 cuts in the tree either way).

Run under trap '<restore>' EXIT INT TERM with an absolute REPO_ROOT, restored with git checkout HEAD -- <path> (never the bare form, which takes the mutation back out of the index), and the restore proven by blob hash AND by an empty git diff HEAD — not by an exit code.

Gates

Derived from the tree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no hand-fed path list; the tool takes its own change set off the merge base), every command run, each exit code captured redirect-then-$?, reconciled with --ran.

30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN — every one exit 0.

0  node scripts/check-ci-filter-parity.mjs                      0  pnpm check:agent-test-spelling
0  node scripts/check-closing-keyword-parity.mjs                0  pnpm check:bash32-floor
0  node scripts/check-closing-keyword-parity.mjs --self-test    0  pnpm check:cli-command-ids
0  node scripts/check-comment-mask-corpus.mjs                   0  pnpm check:cross-package-test-inputs
0  node scripts/check-declaration-mirrors.mjs                   0  pnpm check:declared-population-live
0  node scripts/check-declaration-mirrors.mjs --self-test       0  pnpm check:driver-memory-census
0  node scripts/check-scripts-symbol-anchors.mjs                0  pnpm check:entry-guard
0  node scripts/check-scripts-symbol-anchors.mjs --self-test    0  pnpm check:nul-bytes
0  node scripts/check-self-test-wired.mjs                       0  pnpm check:objectui-bump
0  node scripts/check-self-test-wired.mjs --self-test           0  pnpm check:parse-guard
0  node scripts/check-self-test-workflow-commands.mjs           0  pnpm check:pm-dispatch-gates
0  node scripts/check-self-test-workflow-commands.mjs --self-test  0  pnpm check:pnpm-filter-targets
0  node scripts/check-whole-set-label-write.mjs                 0  pnpm check:ratchet-remedy-authority
0  node scripts/check-whole-set-label-write.mjs --self-test     0  pnpm check:refd-timer-probe
0  node scripts/pm/bare-root-worklist.mjs --self-test           0  pnpm check:watch-hint-literal
✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED
  (a DERIVED zero — all 30 recorded an exit code and none of them is 3).

pnpm check:pm-dispatch-gates is this file's own --self-test and is HEAVY: ✓ dispatch-gates self-test: 1771 cases pass., 633.9s, run DETACHED per this file's own header (#14281) and under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 · held the lock 635s · waited 0s). It exceeds the ~10-minute foreground cap, which is why the header says to detach it.

origin/main moved three commits under this branch mid-run, two of them touching gate scripts, so the derivation was re-run from a scratch worktree at origin/main 84ad2e139 against these two paths: the family list came back byte-identical to the one above — no family was added by the drift.

pnpm lint repo-wide, not narrowed: exit 0 in 84s (node --stack-size=4000 eslint . --no-inline-config, at 3f4bf6b01). The narrowing this lane has been using was not needed on this run.

skip-changeset: scripts/pm/** and scripts/bump-objectui.selftest.sh are in no package's files[] — nothing published moves.


Out-of-scope findings (⛔ not fixed here)

Both are the same CLASS as this card — an author writes a declaration and the consumer silently drops part or all of it — and both are OUTSIDE the file surface triage drew (5713131801), so they are named here for the seat rather than repaired in this PR.

  1. To file — the marker's comment-form alternation is (?:\/\/|#), so a declaration written inside a BLOCK comment parses as nothing at all. Two live specimens: scripts/symbol-anchors.mjs:180 (/* dispatch-gates: no-path-population -- …) and scripts/release-verify-npm.mjs:110 ( * dispatch-gates: no-path-population -- …). Measured: declaredNoPathPopulation returns null for both, both families sit in undetermined with hints=0, and their authors' examined-and-explained status is dropped with no tell — the residue counts them with the families nobody has looked at, which is the exact bucket the marker exists to split. Dedupe words: block comment marker, no-path-population unparsed, symbol-anchors declaration, release-verify-npm population, comment form alternation.
  2. To file — the same first-newline capture is carried by the two markers OUTSIDE this card's three: NO_CHECK_FAMILIES_MARKER (workflow-level, read by declaredNoCheckFamiliesReason) and INHERITED_POPULATION_MARKER (module-level, its reason half). Neither is cut in the tree today (measured: the three live no-check-families declarations and both inherited-population declarations are one-liners followed by a blank line), so this is exposure, not a live defect. The repair is one line each: register the key in POPULATION_MARKER_KEYS / POPULATION_DECLARATION_FIELDS and read the continuation — the helper this PR adds is generic and was deliberately built so the class closes in one move. Dedupe words: no-check-families reason cut, inherited-population reason, population marker wholeness, first newline capture, dispatch-gates marker roster.

Acceptance notes

  • noted, not filed: this file's --self-test has no per-battery floor roster (AGENTS.md 「Writing a --self-test」 asks for battery name → minimum case count); it registers into one flat cases roster. Already recorded — docs/audits/2026-09-self-test-shape-census.md:377 grades it floor NONE, handshake HELD. Not a finding, a censused state. 承接者: whoever works that census row.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu


Generated by Claude Code

…n is red

The three population markers in `scripts/pm/dispatch-gates.mjs` capture their
reason with `(\S.*)$` under the `m` flag, so the capture ends at the first
newline. A reason an author wraps across two or three comment lines was
captured as line one only, and no refusal ever asked whether it ended where the
author did: `wholeTreePopulationRefusal` checks that a reason EXISTS and that a
root walk BACKS it. The seat was handed a sentence that simply stops — and the
reason is the one thing a seat reads off that row.

The marker grammar now has one spelling (`populationMarkerPattern`), and
`populationReasonContinuation` reads, off the same text, the comment line that
continues a reason. A continued reason is refused by
`populationReasonCutRefusal`, which names the declaration, the file and the
line; the whole-tree and wide refusals delegate to it before the checks that
read the reason text, and the no-path renderer prints a cut declaration as
REFUSED rather than as the fragment it was cut down to.

Live derivation over the tree: 27 declarations, one of them cut —
`check:objectui-bump` reached the seat as "every path this file writes or
reads". Its comment is made whole on one line here, verbatim, so no refusal
lands red on main.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@os-justin
os-justin marked this pull request as ready for review September 17, 2026 13:24
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 7f7b855 Sep 17, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18422-dispatch-gates-whole-reason branch September 17, 2026 13:46
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…lock comment, and refuses one it cannot read (objectstack-ai#18784)

Fixes objectstack-ai#18661

Clause-②: no

`scripts/pm/dispatch-gates.mjs`'s three population markers
(`no-path-population`, `whole-tree-population`, `wide-population`) built
their grammar from one head whose comment-form alternation listed `//`
and `#` and nothing else, so a declaration written in a file's own
BLOCK-comment idiom parsed as NOTHING — not refused, not printed, not
counted, and therefore byte-identical in every channel this tool has to
a gate that declares nothing at all. Two live gates were writing one.
The author of each believed they had explained why their gate reads no
population; every reader of the residue saw their family in the
unexamined pile; neither side had anything to check against, which is
what makes this class expensive rather than merely wrong.

## The two before-readings (re-derived on this branch, not taken from
the card)

Taken on `origin/main` `95b21b33be` before any edit:

1. **The population reader answers `null` over both files.**
`declaredNoPathPopulation` returns `null` for
`scripts/symbol-anchors.mjs` and for `scripts/release-verify-npm.mjs`,
and `populationReasonContinuation` returns `null` for both as well — so
not even the objectstack-ai#18422 wholeness refusal had anything to grade. Nothing was
refused because nothing was read.
2. **Both families sit in `undetermined` with `hints=0` and no
annotation.** `node scripts/pm/dispatch-gates.mjs --residue --repo
objectstack-ai/objectstack scripts/pm/dispatch-gates.mjs` placed 309
families — 23 matched, 43 undetermined, 225 silent, 7 always-runs, 11
declared-wide — and reported `9 of those 43 undetermined famil(ies)
DECLARE that they have no path population`. `node
scripts/release-verify-npm.mjs --self-test` and `node
scripts/symbol-anchors.mjs --self-test` are both printed in the
undetermined block with no `names:` suffix and no `↳ declared no path
population` line under them.

One line number on the card was stale and is corrected here:
`scripts/symbol-anchors.mjs`'s declaration is at `:207` on today's tip,
not `:180`; the text is unchanged. `scripts/release-verify-npm.mjs:110`
is where the card says.

## ① The grammar: one roster, five forms, and where a reason ENDS in
each

`MARKER_COMMENT_FORMS` replaces the inline alternation. It is the single
place a form is added, the head both marker builders and the
continuation reading are derived from, and it classifies each form into
one of two KINDS, because the two kinds answer the wholeness question
differently:

| kind | forms | where the reason ends |
|---|---|---|
| `line` | `//`, `#` | on the marker line. A comment line under it in
the SAME form, carrying text that is not a new `dispatch-gates:` key, is
a CUT and is refused — objectstack-ai#18422, unchanged byte for byte |
| `block` | `/**`, `/*`, `*` | at the first of: the closing delimiter, a
blank star-only line, the next star-`@tag` line, another
`dispatch-gates:` key, or EOF. The star-prefixed lines between are
JOINED into the reason |

The block form's own wholeness question is answered in the header rather
than left to be found. Two adjacent `//` lines are two separate comments
and nothing in the text says whether the second belongs to the first —
that is why objectstack-ai#18660 refuses there, and that refusal is untouched. A `/*
... */` block is ONE comment whose internal newlines are formatting, so
its next star line is a continuation *by construction* and the join is
decidable from the text rather than guessed. What the block form cannot
do is cross any of the three places a block-comment author signals a new
thought; each is pinned. A line INSIDE the block carrying text with no
star prefix is none of the five endings — it is reason text the walk
cannot read — so it is recorded as the block form's CUT and refused with
its own remedy ("give that line the block's star prefix, or end the
reason before it with a blank star line"), never with the line forms'
advice, which would send that author to the wrong half of their
declaration.

The one residual asymmetry is named in the header rather than hidden: a
second sentence on the very next star line, with no blank line between,
IS swallowed. That is OVER-inclusion and it reaches a seat as a reason
that says too much — visible on the row. The truncation objectstack-ai#18422 refused
is UNDER-inclusion and reaches a seat as a sentence that merely ends
oddly — invisible. The block idiom's own paragraph break is the text
that separates the two, and it is what a block-comment author already
writes.

⛔ What a population declaration MEANS is unchanged, and no refusal PR
objectstack-ai#18660 added is loosened: the line forms' capture, their continuation
reading and their refusal text are all identical, and a self-test case
asserts that refusal text still reads "Put the WHOLE reason on the
marker line".

## ② The sound: a line that reads like a declaration and did not parse
is never silent

`unparsedPopulationMarkers` + `unparsedPopulationMarkerRefusal`, RED at
author time through this file's own `--self-test` live half, which
sweeps every gate source the discovery reads (250 sources on this tree)
and names each finding by FILE, LINE and the FORM it was written in —
exactly the three things the old output withheld. Two causes,
deliberately one finding, because the author's experience is identical:
an unrecognised comment form, and a recognised form with no `-- REASON`
tail.

The chosen place is the refusal rather than the derivation output,
because the refusal fires before any row can be printed: an unparsed
declaration cannot reach `main`, so a second rendering of a state that
cannot exist would be unreachable code, which this file's own rule
refuses.

Two boundaries, both measured rather than assumed. A line may carry AT
MOST ONE comment opener — a line with the docblock's own star plus a
second opener is an EXAMPLE of a declaration written inside a comment
about declarations, which is how every example in this file is written,
and the grammar reads one opener too, so probe and grammar agree about
what documentation looks like. And a QUOTE is not a comment opener:
every self-test in this family builds its fixtures out of string
literals, this file's own included, and the grammar already ignores them
for the same reason.

## Census — the whole tree, report-only, no state written

Swept over all 8842 tracked files at `95b21b33be`:

| reading | count |
|---|---|
| lines carrying the literal text `dispatch-gates:` | 137 |
| of those, lines carrying a POPULATION key | 69 |
| parse as a declaration under the OLD form set (`//`, `#`) | 25, across
25 files |
| parse under the NEW form set (+ `/**`, `/*`, `*`) | 27, across 27
files |
| NEWLY parsing | 2 — `scripts/release-verify-npm.mjs:110`,
`scripts/symbol-anchors.mjs:207` |
| declaration-SHAPED and unparsed on `main` (what ② would have flagged)
| 2 — the same two |
| still unparsed after ① | **0** |

So ②'s only live cases were the two ① repairs, and the census is its
coverage statement: the remaining 67 population-key lines are prose
mentions (5 of them, all backticked or mid-sentence), this file's own
docblock examples, and string-literal fixtures — none is
declaration-shaped, and the live sweep over the tip returns zero. The
pins for ② are therefore synthetic plus one live non-vacuity leg that
takes a real declaration, re-spells its opener in an unlisted form, and
asserts exactly that line is found.

## The family before/after — nothing else moves

Same command, same probe path, before at `95b21b33be` and after at
`98d0b1dca4`; the full listings differ by 28 lines and every movement is
named:

- `+ ↳ declared no path population — ...` under `node
scripts/release-verify-npm.mjs --self-test` — this change
- `+ ↳ declared no path population — ...` under `node
scripts/symbol-anchors.mjs --self-test`, with the whole six-line reason
joined — this change
- `9 of those 43 undetermined` becomes `11 of those 43` — this change
- `check-dev-prereqs` 70 declared literals becomes 71, and its `names:`
list grows — NOT this change: `scripts/check-dev-prereqs.mjs` moved on
`origin/main` in the merge this branch carries
- `8842 tracked file(s)` becomes `8844` (two places) — NOT this change:
the merge added `.changeset/amplifiers-linked-packages.md` and
`scripts/measure-markdown-ts-blocks.mjs`
- the derivation header's commit sha

The five bucket counts are byte-identical: 23 matched, 43 undetermined,
225 silent, 7 always-runs, 11 declared-wide. No family changed bucket.
`no-path-population` annotates WITHIN `undetermined` rather than moving
a family out of it, which is what the channel does.

## The ledger row the readable declaration graduates

`ROOT_WALK_RESIDUE_LEDGER` carried a hand-written row for
`scripts/symbol-anchors.mjs --self-test` — a repo-root walker that
declares neither marker and cannot be placed by path. It declared
neither marker only because its declaration could not be read. With the
form set widened it leaves that population by DECLARING, which the
table's own header names as the outcome it exists to push toward, and
`check:pm-dispatch-gates` reds on a stale exclusion by design. The row
is deleted in the same landing, with a `⚖️` note in the style the table
already uses for its one previous graduation. This is the card's defect
priced in a second currency: it had cost a hand-maintained exclusion
row, carrying by hand the reading the gate's own source already carried.

## Ablation (from the committed fix, restored under a trap)

Mutation: the three `kind: 'block'` rows deleted from
`MARKER_COMMENT_FORMS`, proven on disk by blob hash —
`e72c4d48d33ef670c7baa16f3929cf3f4bf958f5` (== the HEAD blob) becomes
`1c2128d800e60ffa93c81cbad76313c381f69c92`.

| ablated reading | result |
|---|---|
| `declaredNoPathPopulation` over the two live files | both back to
`null` |
| residue's documented-no-population count | 11 back to 9 |
| `pnpm check:pm-dispatch-gates` | **exit 1** — `11 of 1809 case(s)
failed` |
| the 8 block-form grammar pins | all RED |
| the live pin "the live tree's BLOCK-form declarations are READ and not
dropped" | RED, `found 0: none` |
| the SOUND, over the real tree | RED, and it names both specimens:
`scripts/release-verify-npm.mjs:110 declares no-path-population in form
*` and `scripts/symbol-anchors.mjs:207 declares no-path-population in
form /*` |
| the ledger case | RED in the OPPOSITE direction — `unlisted:
scripts/symbol-anchors.mjs --self-test` — so the row's deletion is
coupled to the fix by construction |
| the other 1798 cases | green, unchanged |

Restore: `git checkout HEAD -- scripts/pm/dispatch-gates.mjs` under an
`EXIT INT TERM` trap, verified by hash equality with the HEAD blob AND
an empty `git diff HEAD`, not by an exit code.

## Self-test — measured, not NOT MEASURED

`pnpm check:pm-dispatch-gates` run DETACHED with its output to a file
and read from the file, never under a foreground timeout: **exit 0**, `✓
dispatch-gates self-test: 1809 cases pass`, battery 748.0s on this box,
at `71aacfb886`.

## Derived gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed path list: 1
path in the change set, 28 commands derived. Every one run, each exit
code captured by redirect-then-`$?`, and reconciled:

`✓ dispatch-gates --ran: 28 derived famil(ies) accounted for — 28 run, 0
NOT-MEASURED (a DERIVED zero — all 28 recorded an exit code and none of
them is 3)`

All 28 exit 0. Repo-wide `pnpm lint` (`eslint . --no-inline-config`):
exit 0. The derivation at `71aacfb886` prints a STALE TREE note naming
one file that moved on `origin/main` afterwards,
`scripts/pm/post-stamped.mjs`; the derived command list is
byte-identical before and after that move, and the queue rebuilds this
PR on the current `main` regardless.

## Scope

⛔ objectstack-ai#18662 is NOT folded in. It asks for the REASON-WHOLENESS reading (the
cut refusal) to be extended to `no-check-families` and
`inherited-population`; this change gives neither of them one, and
`NO_CHECK_FAMILIES_MARKER` is a separate `#`-only regex for YAML that is
untouched. One interaction is worth recording: `pathListMarkerPattern`
shares the one head with the population markers by construction
(objectstack-ai#18673), so `inherited-population` and `self-test-reads` now also
accept the block spellings. There are zero live block-form declarations
of either key — both live ones are `//` — so nothing moves, and their
reason-wholeness exposure is exactly what objectstack-ai#18662 describes, neither
widened nor narrowed here.

`skip-changeset`: the diff is one file under `scripts/pm/**`, a PM loop
tool that no package's `files[]` ships.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…oster the objectstack-ai#18422 reason-wholeness reading (objectstack-ai#18822)

Fixes objectstack-ai#18662

Clause-②: no

## The defect

`NO_CHECK_FAMILIES_MARKER` and `INHERITED_POPULATION_MARKER`'s reason
half captured their reason with the same `(\S.*)$`-under-`m` shape that
objectstack-ai#18422 (PR objectstack-ai#18660) repaired for the three population markers: the
capture ends at the FIRST NEWLINE, so a reason an author wraps onto the
comment line below is read as line ONE and nothing refuses it. Neither
marker was in `POPULATION_MARKER_KEYS`, so neither
`populationReasonContinuation` nor `populationReasonCutRefusal` could
even be asked about them — both threw `unknown population marker key`.
No live declaration is cut in this tree today (the census below), so
this is exposure, not a live cut; the cost lands on the next author who
wraps one and the seat who reads the sentence that stops.

## The before-reading, re-derived on `main` (not taken from the card)

Measured against `origin/main` `034f5a3afd`, feeding each marker a
reason wrapped over two comment lines — the same wrapped shape objectstack-ai#18422
measured for the population markers.

`no-check-families`, consumer `declaredNoCheckFamiliesReason` (the
capture at `scripts/pm/dispatch-gates.mjs:2707`, `return m ? m[1].trim()
: null;` at :2708):

```text
reason read      : "steps are an install/build/boot pipeline, and the verdict is"
author wrote     : "steps are an install/build/boot pipeline, and the verdict is whether the
                    scaffolded app boots at all, which no named local check family covers"
consumer sounds? : checkFamilyCoverageGaps -> []   (empty = accepted, silently)
wholeness reading: THROWS -- unknown population marker key 'no-check-families'
```

`inherited-population`, consumer `declaredInheritedPopulation` (the
reason at :4154, `const reason = m[3].trim();`):

```text
population read  : [".github/workflows"]
reason read      : "the workflow directory this module readdirs, and the verdict is"
consumer sounds? : no throw, no refusal, no row — the call returned normally
wholeness reading: THROWS -- unknown population marker key 'inherited-population'
```

Control, the same wrapped shape on a population marker (objectstack-ai#18422
repaired):

```text
populationReasonContinuation(..., 'wide-population', 'scripts/x.mjs')
  -> {"file":"scripts/x.mjs","line":2,"text":"the count of packages whose manifest this gate refuses","kind":"line"}
```

That is the red this change turns green.

## The registration, and where the refusal prints

One grammar, no second pattern, no hand-rolled continuation reader.

- `MARKER_KEY_FORMS` / `markerLineHead(key)` — the shared head, built
from `MARKER_COMMENT_FORMS` FILTERED to the forms that key's language
has.
- `REASON_TAIL_MARKER_KEYS = [...POPULATION_MARKER_KEYS,
'no-check-families']` — `no-check-families` now comes out of
`populationMarkerPattern` instead of the fourth hand-written copy of the
same grammar it was. The builder keeps its name because `population*` is
what this machinery is called where it is exported; the roster, not the
name, is the authority on which keys it serves, and the docblock says
so.
- `MARKER_REASON_GRAMMARS` — which builder reads each key and which
capture group holds its reason, derived BY CONSTRUCTION from the two
builders' own key rosters rather than hand-listed. That is what closed
the class: a key cannot be added to either builder without the wholeness
reading arriving with it. It is also why `self-test-reads` — the sixth
reason-bearing key, same grammar, same defect, filed under objectstack-ai#18673 and
NOT named on this card — is covered in the same line rather than
becoming a third card on this file. See "Scope" below.
- `readPopulationMarker` reads its reason out of the group the roster
names, and returns the raw match so a path-list reader takes its path
list off the SAME read its reason came from.
- `markerReasonCutRefusal(markerKey, cut)` — the refusal TEXT, one copy,
five keys. `populationReasonCutRefusal(entry, markerKey)` is now that
function reached through a discovery entry, and its output is
byte-identical to what it was before the split (pinned).

Where it prints: the three population channels carry their declaration
into a discovery entry that IS rendered, so their refusal is a printed
row. These markers have no such row (see the next section), so the READ
refuses — `refuseCutMarkerReason` throws, the CLI catches it at its
`catch (err)` and prints `dispatch-gates: derivation failed —` followed
by the refusal text, exit 2. That is already how both path-list markers
refuse an invented path. The message names the file, the line, the
marker and the continuation it saw:

```text
dispatch-gates: .github/workflows/scaffold-e2e.yml declares no-check-families and its reason does not
END on the marker line: .github/workflows/scaffold-e2e.yml:7 continues it with "whether the scaffolded
app boots at all, which no named local check family covers". The capture stops at the FIRST NEWLINE, so
the seat is handed the declaration cut off mid-sentence — ... Put the WHOLE reason on the marker line,
however long it runs ... ⛔ Never widen the marker to swallow the next line instead ...
```

The second surface is this file's own `--self-test` live census, below —
the same place the population markers' live half reds.

⛔ What either declaration MEANS is unchanged: the `inherited-population`
path list is untouched, its invented-path refusal is unchanged and
pinned independently, `no-check-families` still exempts exactly the
workflows it exempted, and no refusal PR objectstack-ai#18660 / PR objectstack-ai#18784 added is
loosened.

## The card's 「not measured」, answered: NO, neither consumer renders the
reason

Read to the surface the text lands on, at `034f5a3afd`:

- `declaredNoCheckFamiliesReason` has exactly ONE consumer —
`checkFamilyCoverageGaps` at :4298, `if
(declaredNoCheckFamiliesReason(text)) continue;`. The return value is
read as a BOOLEAN. The reason string reaches no row, no log and no seat.
- `declaredInheritedPopulation` has three production call sites
(`discoverFamilies`' `hintsOfModule`, and two in this file's own
self-test) plus the governed-read census; every one of them reads
`.population`. The only read of `.reason` in the tree is a self-test
case asserting it is non-empty.

So a cut costs nothing to a RENDERING today. The refusal is owed anyway
and is stated in both docblocks rather than hidden: wholeness is a
property of the declaration, not of today's consumer, and the next
reader of either reason is the seat that greps the workflow or the
module for it — which is exactly the reader the population markers'
repair was written for. The cost is that this refusal buys a seat's
future read, not a row on today's output.

## PR objectstack-ai#18784's block-comment forms, measured against these two

Measured both directions, one form per row, on `034f5a3afd` and on this
branch.

BEFORE — `MARKER_COMMENT_FORMS` reached `no-check-families` in NO form
at all: its pattern was a hand-written regex that never consulted the
roster, so `#` matched because it was hard-coded there and the other
four did not. It reached `inherited-population` in ALL FIVE forms
already (that marker is built on the shared head since objectstack-ai#18673/objectstack-ai#18661) —
but its reason was `m[3].trim()`, so a block-form reason kept the
closing delimiter and a wrapped block reason was cut at the first
newline:

```text
BEFORE  no-check-families:      #  -> "a reason */"   //  -> null   /*  -> null   /** -> null   *  -> null
BEFORE  inherited-population:   #  -> "a reason */"   //  -> "a reason */"   /*  -> "a reason */"
        block WRAPPED reason -> "the workflow directory this module readdirs, and"      (cut)
```

AFTER — `no-check-families` is built from the roster FILTERED to `#`,
and the four other forms stay null. ⛔ That is not a widening withheld;
it is the answer to the question: **the block form cannot apply to a
YAML `#` marker.** `#` is the only comment syntax YAML has — a `//` or
slash-star line in a workflow is document content, not a remark, so
reading a declaration off one would be reading it off text the
workflow's own parser never treats as a comment. The
`inherited-population` rows are unchanged in form coverage; only its
block-form reason now ends where the block ends:

```text
AFTER   no-check-families:      #  -> "a reason */"   //  -> null   /*  -> null   /** -> null   *  -> null
AFTER   inherited-population:   #  -> "a reason */"   //  -> "a reason */"   /*  -> "a reason"
        block WRAPPED reason -> "the workflow directory this module readdirs, and every other literal
                                 here is a join base"                                    (whole)
```

## Scope — and the sixth key

The card names two markers. The registration is derived from the two
grammar builders' key rosters, so it also reaches `self-test-reads`
(objectstack-ai#18673), the sixth reason-bearing key. That is deliberate, and it is
the bounded in-place fix rather than scope creep — the four conditions,
answered: (1) same defect class as this card, the identical
first-newline capture; (2) mechanical, and the shape is pinned by the
two markers this card names; (3) no other claim holds this file (28 open
PRs' file lists read at dispatch time — none touches it; objectstack-ai#18536's
machine-side half is queued behind this card, not claimed); (4) same
gate family, no new verification surface. The alternative was a roster
that hand-lists five of six reason-bearing keys — a second copy of
"which keys have a reason", wrong in the silent direction the moment a
seventh arrives, which is exactly how these two sat outside objectstack-ai#18422 for
two cards.

⛔ objectstack-ai#18661 / PR objectstack-ai#18784 is read and not re-opened: its form roster and its
dropped-declaration sound are untouched. ⛔ objectstack-ai#18536's machine-side half is
not folded in — nothing in this diff touches half-state or Clause-②
machinery.

## Pins — 26 new cases in `--self-test`

Per marker (`no-check-families`, `inherited-population`,
`self-test-reads`):

- a wrapped-reason case REFUSED by name, asserting the message carries
the file, the `file:line` of the continuation, the marker key and the
continuation's text — the refusal text pinned, not paraphrased;
- a whole-reason control that still reads its reason (and, for
`no-check-families`, that the workflow is still not a coverage gap) —
the repair refuses a cut, it does not refuse the marker.

Plus: the refusal reaching `checkFamilyCoverageGaps`, the marker's one
consumer; a blank-line-separated comment still not a continuation (the
terminator all six live declarations write); `inherited-population`'s
invented-path refusal pinned as INDEPENDENT of the cut refusal;
`self-test-reads`' missing-read-set refusal still firing first; the
`#`-only restriction both ways, plus the restriction narrowing-only and
its drift guard driven through a bad table; the wholeness roster pinned
equal to the two builders' rosters and named at all six keys; the reason
GROUP pinned per builder; and `populationReasonCutRefusal` pinned
byte-equal to `markerReasonCutRefusal` so the split cannot drift.

The population markers' existing pins are byte-unchanged.

## The census — the five (six) live declarations, measured whole

Taken in `--self-test` against the real tree at the head on every run,
not printed once into this body. Each row is NAMED, never counted:

```text
.github/workflows/merged-branch-reaper.yml:212  no-check-families
.github/workflows/os-create-smoke.yml:48        no-check-families
.github/workflows/scaffold-e2e.yml:23           no-check-families
scripts/cli-build-prerequisite.mjs:111          inherited-population
scripts/pm/check-expected-skips.mjs:131         self-test-reads
scripts/pm/dispatch-gates.mjs:702               inherited-population
```

The card's five, plus the `self-test-reads` declaration the sixth key
brings. All six are one-liners followed by a blank line — no reason is
cut in this tree today, which is the card's own reading, re-taken. The
case asserts three things separately: the roster is exactly those six,
every reason ENDS on its marker line, and every reason is non-empty. A
non-vacuity control puts a continuation under a LIVE declaration and
asserts that exact file is refused by name, so the census cannot pass by
measuring nothing.

## Ablation

From the COMMITTED fix, the registration removed on disk — the three
`refuseCutMarkerReason(...)` calls, which is the mutation shape this
file already names for this family ("deleting the
`populationReasonCutRefusal` call from either refusal"). Mutation proven
by blob hash before the run, restored by hash under a `trap ... EXIT INT
TERM` after it:

```text
head           : 0538995
head blob      : 1d4c953
on-disk before : 1d4c953
anchor count before: 3   (expect 3)
anchor count after : 0   (expect 0)
on-disk after  : 5ea16c9a050769027d876e39f8b7a0d21c439a03
MUTATION: PROVEN — blob hash moved and 3 anchor line(s) left the file

✗ dispatch-gates self-test: 5 of 1835 case(s) failed.
os-verify-lock: VERDICT command-exit 1 · held the lock 745s (12m25s) · waited 1s

RESTORE: hash-on-disk=1d4c95393c3749e68129dc2688e8df8c351aaa9b head-blob=1d4c95393c3749e68129dc2688e8df8c351aaa9b
RESTORE: OK — byte-identical to HEAD
RESTORE: git diff HEAD (must be empty):        [empty]
```

The five, and only the five, are the new refusal pins — one per marker,
plus the consumer-reaching case and the census non-vacuity control:

```text
✗ a no-check-families reason that does not END on the marker line is REFUSED, naming the workflow, the line, the marker and the continuation
✗ and the refusal reaches the ONE consumer this marker has — the boolean read in checkFamilyCoverageGaps refuses rather than accepting half a sentence
✗ an inherited-population reason that does not END on the marker line is REFUSED, naming the module, the line, the marker and the continuation
✗ the census is not vacuous over this tree: put a continuation under a LIVE declaration and exactly that file is refused, by name
✗ a self-test-reads reason that does not END on the marker line is REFUSED too, in the same words and naming the same four things
```

Nothing pre-existing reds. The continuation READING, the whole-reason
controls, the roster pins and every population-marker pin stay green
under the ablation — which is the point: this change registers a
refusal, it does not change how a reason is read.

## The self-test line

Run DETACHED with output to a file and waited on in the foreground
(`tail --pid` on the detached pid), never under a foreground timeout,
and under the shared verify lock:

```text
✓ dispatch-gates self-test: 1835 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 734s (12m14s) · waited 0s
```

1809 before this change, 1835 after — 26 new cases. Shared-box seconds,
not idle-box figures.

## Derived gates, each with its exit code

Derived from the worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (no hand-fed path list),
every command run with the exit code captured by redirect-then-`$?`,
then reconciled with `--ran`.

```text
node scripts/check-ci-filter-parity.mjs                        :: exit 0
node scripts/check-closing-keyword-parity.mjs                  :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test      :: exit 0
node scripts/check-comment-mask-corpus.mjs                     :: exit 0
node scripts/check-declaration-mirrors.mjs                     :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test         :: exit 0
node scripts/check-scripts-symbol-anchors.mjs                  :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test      :: exit 0
node scripts/check-self-test-wired.mjs                         :: exit 0
node scripts/check-self-test-wired.mjs --self-test             :: exit 0
node scripts/check-self-test-workflow-commands.mjs             :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs                   :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test       :: exit 0
pnpm check:agent-test-spelling                                 :: exit 0
pnpm check:bash32-floor                                        :: exit 0
pnpm check:cli-command-ids                                     :: exit 0
pnpm check:cross-package-test-inputs                           :: exit 0
pnpm check:declared-population-live                            :: exit 0
pnpm check:driver-memory-census                                :: exit 0
pnpm check:entry-guard                                         :: exit 0
pnpm check:nul-bytes                                           :: exit 0
pnpm check:parse-guard                                         :: exit 0
pnpm check:pm-dispatch-gates                                   :: exit 0   (the detached run above)
pnpm check:pnpm-filter-targets                                 :: exit 0
pnpm check:ratchet-remedy-authority                            :: exit 0
pnpm check:refd-timer-probe                                    :: exit 0
pnpm check:watch-hint-literal                                  :: exit 0
```

```text
✓ dispatch-gates --ran: 28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED
  (a DERIVED zero — all 28 recorded an exit code and none of them is 3).
```

Repo-wide `pnpm lint` (`eslint . --no-inline-config`): exit 0.

`skip-changeset`: the diff is one file under `scripts/pm/`, which no
package's `files[]` ships — nothing published moves.

## Acceptance notes

To file (class (c), a declaration the tool silently drops), NOT fixed
here — different defect class from this card, so the bounded in-place
exemption does not apply: **the dropped-declaration probe
`unparsedPopulationMarkers` (objectstack-ai#18661) is keyed on
`POPULATION_MARKER_KEYS` alone**, so a line that READS as a
`no-check-families`, `inherited-population` or `self-test-reads`
declaration and does not PARSE as one makes no sound at all — no
refusal, no row, no count — while the identical shape on a population
key is reported. Measured on `034f5a3afd`, three dropped spellings
returned `[]` from the probe with a population-key control lighting on
the same opener. The `self-test-reads` instance re-opens objectstack-ai#18673's own
hole: a dropped declaration takes a family out of the derived set, and
the `--ran` reconciliation then says "0 NOT-MEASURED" about a set that
no longer contains it. Dedupe words: `unparsed marker probe roster` ·
`dropped declaration silent` · `POPULATION_MARKER_LOOKALIKE keys` ·
`inherited-population dropped` · `self-test-reads dropped`.

Noted, not filed: routing `inherited-population`'s block-form read
through `blockFormReason` also drops the closing delimiter that
`m[3].trim()` used to leave in the reason (`"a reason */"` becomes `"a
reason"`). No live declaration is written in a block form, so nothing
moved in this tree; it is measured in the block-form table above rather
than left to be found. Next reader of this file: objectstack-ai#18536's machine-side
half.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants